<template>
    <MyArticle class="draw-clock-article" title="使用canvas绘制一面动态的时钟" date="2016年06月17日">
        <div class="_article-content">
            <div class="exp" ref="grid">
                <canvas ref="clock" width="400" height="400" class="clock"></canvas>
            </div>
            <h3 class="title">代码如下：</h3>
            <p>data定义：</p>
            <pre><code>data () {
    return {
        context: null,
        centerX: 0, // 中心点
        centerY: 0,
        ringInnerRadius: 150, // 内圆环半径
        ringOuterRadius: 170, // 外圆环半径
        hourTickLen: 10, // 大刻度长度
        minTickLen: 5, // 小刻度长度
        lastTime: 0 // 上次的时间
    }
}</code></pre>
            <p>初始化部分：</p>
            <pre><code>init () {
    let that = this
    let refs = that.$refs
    let el = refs.clock
    let box = refs.grid
    let rect = box.getBoundingClientRect()
    let w = (rect.right - rect.left) * 0.95
    if (w &lt; 400) {
        el.style.width = w + 'px'
        el.style.height = w + 'px'
    }
    let canvasWidth = el.getAttribute('width')
    let canvasHeight = el.getAttribute('height')
    that.centerX = canvasWidth / 2
    that.centerY = canvasHeight / 2
    that.context = el.getContext('2d')
    that.startTimer()
}</code></pre>
            <p>绘制整个时钟的方法：</p>
            <pre><code>drawClock () {
    let that = this
    if (parseInt(that.lastTime / 1000) != parseInt((+new Date()) / 1000)) {
        that.drawGrid()
        that.drawCentroid()
        that.drawRing()
        that.drawTicks()
        that.drawPoints()
    }
}</code></pre>
            <p>随着时间重绘：</p>
            <pre><code>startTimer () {
    let that = this
    let requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame
    requestAnimationFrame(function () {
        that.drawClock()
        that.startTimer()
    })
}</code></pre>
            <p>绘制底层网格：</p>
            <pre><code>drawGrid () {
    let that = this
    let context = that.context
    let el = context.canvas
    let w = el.width
    let h = el.height
    let stepX = 10
    let stepY = 10

    context.clearRect(0, 0, w, h)
    context.save()
    context.beginPath()
    context.strokeStyle = 'rgba(0,0,0,0.05)'
    context.lineWidth = 0.5
    for (let i = stepX + 0.5; i &lt; w; i += stepX) {
        context.beginPath()
        context.moveTo(i, 0)
        context.lineTo(i, h)
        context.stroke()
    }

    for (let j = stepY + 0.5; j &lt; h; j += stepY) {
        context.beginPath()
        context.moveTo(0, j)
        context.lineTo(w, j)
        context.stroke()
    }
    context.restore()
}</code></pre>
            <p>绘制中心点：</p>
            <pre><code>drawCentroid () {
    let that = this
    let context = that.context
    context.save()
    context.beginPath()
    context.strokeStyle = 'rgba(0,0,0,0.3)'
    context.fillStyle = 'rgba(0,0,0,0.25)'
    context.arc(that.centerX, that.centerY, 10, 0, 2 * Math.PI, true)
    context.stroke()
    context.fill()
    context.restore()
}</code></pre>
            <p>绘制圆环：</p>
            <pre><code>drawRing () {
    let that = this
    let context = that.context
    context.save()
    context.beginPath()

    // 外侧的圆
    context.shadowColor = 'rgba(0,0,0,0.7)'
    context.shadowOffsetX = 4
    context.shadowOffsetY = 4
    context.shadowBlur = 8
    context.fillStyle = 'rgba(0,0,0,0.16)'
    context.strokeStyle = 'rgba(0,0,0,0.25)'
    context.arc(that.centerX, that.centerY, that.ringOuterRadius, 0, 2 * Math.PI, false)
    context.stroke()
    // 内侧的圆
    context.strokeStyle = 'rgba(0,0,0,0.2)'
    context.arc(that.centerX, that.centerY, that.ringInnerRadius, 0, 2 * Math.PI, true)
    context.fill()
    context.beginPath()
    context.arc(that.centerX, that.centerY, that.ringInnerRadius, 0, 2 * Math.PI, true)
    context.stroke()
    context.restore()
}</code></pre>
            <p>绘制刻度：</p>
            <pre><code>drawTicks () {
    let that = this
    let context = that.context
    let hourTickRadius = that.ringInnerRadius - that.hourTickLen
    let minTickRadius = that.ringInnerRadius - that.minTickLen
    let pi = Math.PI
    let startEngle = -(pi / 2)
    let deltEngle = 2 * pi / (4 * 12)
    let startPointX = 0
    let startPointY = 0
    let endPointX = 0
    let endPointY = 0
    let currEngle = startEngle
    let currTickLen
    let num

    context.save()
    context.lineWidth = 1
    context.textAlign = 'center'
    context.textBaseline = 'middle'
    context.font = '12px 微软雅黑 normal'
    for (let i = 0; i &lt; 48; i++) {
        currTickLen = (i % 4 == 0 ? hourTickRadius : minTickRadius)
        context.lineWidth = (i % 4 == 0 ? 2 : 1)
        context.strokeStyle = 'rgba(0,0,0,0.2)'
        startPointX = that.centerX + Math.cos(currEngle) * currTickLen
        startPointY = that.centerY + Math.sin(currEngle) * currTickLen
        endPointX = that.centerX + Math.cos(currEngle) * that.ringInnerRadius
        endPointY = that.centerY + Math.sin(currEngle) * that.ringInnerRadius
        context.beginPath()
        context.moveTo(startPointX, startPointY)
        context.lineTo(endPointX, endPointY)
        context.stroke()
        if (i % 4 == 0) { // 绘制数字
            num = i / 4
            num = (num == 0 ? 12 : num)
            context.strokeStyle = 'rgba(0,0,0,0.5)'
            context.strokeText(num, that.centerX + Math.cos(currEngle) * (currTickLen - 10), that.centerY + Math.sin(currEngle) * (currTickLen - 10))
        }
        currEngle += deltEngle
    }
    context.restore()
}</code></pre>
            <p>绘制指针以及时间文本：</p>
            <pre><code>drawPoints () {
    let that = this
    let context = that.context
    let pi = Math.PI
    let currDate = new Date()
    let hour = currDate.getHours()
    let min = currDate.getMinutes()
    let second = currDate.getSeconds()
    let hourPointLen = 55
    let hourEngle = -(pi / 2) + (2 * pi / 12) * (hour % 12)
    let minPointLen = 85
    let minEngle = -(pi / 2) + (2 * pi / 60) * min
    let secondPointLen = 115
    let secondEngle = -(pi / 2) + (2 * pi / 60) * second
    that.lastTime = currDate.getTime()
    context.save()
    context.lineCap = 'round'
    context.strokeStyle = 'rgba(0,0,0,0.5)'
    // 小时的指针
    context.lineWidth = 4
    context.beginPath()
    context.moveTo(that.centerX, that.centerY)
    context.lineTo(that.centerX + hourPointLen * Math.cos(hourEngle), that.centerY + hourPointLen * Math.sin(hourEngle))
    context.stroke()

    // 分钟的指针
    context.lineWidth = 3
    context.beginPath()
    context.moveTo(that.centerX, that.centerY)
    context.lineTo(that.centerX + minPointLen * Math.cos(minEngle), that.centerY + minPointLen * Math.sin(minEngle))
    context.stroke()

    // 秒的指针
    context.lineWidth = 2
    context.beginPath()
    context.moveTo(that.centerX, that.centerY)
    context.lineTo(that.centerX + secondPointLen * Math.cos(secondEngle), that.centerY + secondPointLen * Math.sin(secondEngle))
    context.stroke()

    // 当前时间文本
    let timeStr = (hour &lt; 10 ? '0' + hour : hour) + ' : ' + (min &lt; 10 ? '0' + min : min) + ' : ' + (second &lt; 10 ? '0' + second : second)
    context.font = '14px 微软雅黑 normal'
    context.textAlign = 'center'
    context.textBaseline = 'middle'
    context.strokeStyle = 'rgba(0,0,0,0.5)'
    context.lineWidth = 1
    context.strokeText(timeStr, that.centerX, that.centerY + 45)
    let month = currDate.getMonth() + 1
    let date = currDate.getDate()
    let day = currDate.getDay()
    let weekObj = ['日', '一', '二', '三', '四', '五', '六']
    timeStr = currDate.getFullYear() + ' / ' + (month &lt; 10 ? '0' + month : month) + ' / ' + (date &lt; 10 ? '0' + date : date) + ' 星期' + weekObj[day]
    context.strokeText(timeStr, that.centerX, that.centerY + 75)
    context.restore()
}</code></pre>
        </div>
    </MyArticle>
</template>

<script>
import { markRaw } from 'vue'
export default {
    data () {
        return {
            context: null,
            centerX: 0, // 中心点
            centerY: 0,
            ringInnerRadius: 150, // 内圆环半径
            ringOuterRadius: 170, // 外圆环半径
            hourTickLen: 10, // 大刻度长度
            minTickLen: 5, // 小刻度长度
            lastTime: 0,
            aniId: 0,
            scale: 1
        }
    },
    mounted () {
        const that = this
        that.init() // 绘制
        window.addEventListener('resize', that.redraw)
    },
    beforeUnmount () {
        const that = this
        if (that.aniId) {
            window.cancelAnimationFrame(that.aniId)
        }
        window.removeEventListener('resize', that.redraw)
    },
    methods: {
        redraw () {
            const that = this
            if (that.aniId) {
                window.cancelAnimationFrame(that.aniId)
            }
            that.init()
        },
        setSize () {
            const that = this
            const refs = that.$refs
            const el = refs.clock
            const box = refs.grid
            const rect = box.getBoundingClientRect()
            const w = rect.right - rect.left
            const h = rect.bottom - rect.top
            const scale = window.devicePixelRatio || 1
            el.style.width = w + 'px'
            el.style.height = h + 'px'
            const canvasWidth = w * scale
            const canvasHeight = h * scale
            el.width = canvasWidth
            el.height = canvasHeight
            that.centerX = canvasWidth / 2
            that.centerY = canvasHeight / 2
            that.scale = scale

            that.ringInnerRadius = 150 * scale // 内圆环半径
            that.ringOuterRadius = 170 * scale // 外圆环半径
            that.hourTickLen = 10 * scale // 大刻度长度
            that.minTickLen = 5 * scale // 小刻度长度
        },
        // 启动绘制
        init () {
            const that = this
            const refs = that.$refs
            const el = refs.clock
            that.setSize()
            that.context = markRaw(el.getContext('2d'))
            that.startTimer()
        },
        // 随着时间走
        startTimer () {
            const that = this
            const requestAnimationFrame = window.requestAnimationFrame
            that.aniId = requestAnimationFrame(function () {
                that.drawClock()
                that.startTimer()
            })
        },
        // 绘制时钟
        drawClock () {
            const that = this
            if (parseInt(that.lastTime / 1000) != parseInt((+new Date()) / 1000)) {
                that.drawGrid()
                that.drawCentroid()
                that.drawRing()
                that.drawTicks()
                that.drawPoints()
            }
        },
        // 绘制网格
        drawGrid () {
            const that = this
            const context = that.context
            const el = context.canvas
            const w = el.width
            const h = el.height
            const stepX = 10
            const stepY = 10

            context.clearRect(0, 0, w, h)
            context.save()
            context.beginPath()
            context.strokeStyle = 'rgba(0,0,0,0.05)'
            context.lineWidth = 0.5
            for (let i = stepX + 0.5; i < w; i += stepX) {
                context.beginPath()
                context.moveTo(i, 0)
                context.lineTo(i, h)
                context.stroke()
            }

            for (let j = stepY + 0.5; j < h; j += stepY) {
                context.beginPath()
                context.moveTo(0, j)
                context.lineTo(w, j)
                context.stroke()
            }
            context.restore()
        },
        // 绘制中心点
        drawCentroid () {
            const that = this
            const context = that.context
            context.save()
            context.beginPath()
            context.strokeStyle = 'rgba(0,0,0,0.3)'
            context.fillStyle = 'rgba(0,0,0,0.25)'
            context.arc(that.centerX, that.centerY, 10 * that.scale, 0, 2 * Math.PI, true)
            context.stroke()
            context.fill()
            context.restore()
        },
        // 绘制圆环
        drawRing () {
            const that = this
            const context = that.context
            context.save()
            context.beginPath()

            // 外侧的圆环
            context.shadowColor = 'rgba(0,0,0,0.7)'
            context.shadowOffsetX = 4
            context.shadowOffsetY = 4
            context.shadowBlur = 8
            context.fillStyle = 'rgba(0,0,0,0.16)'
            context.strokeStyle = 'rgba(0,0,0,0.25)'
            context.arc(that.centerX, that.centerY, that.ringOuterRadius, 0, 2 * Math.PI, false)
            context.stroke()
            context.strokeStyle = 'rgba(0,0,0,0.2)'
            context.arc(that.centerX, that.centerY, that.ringInnerRadius, 0, 2 * Math.PI, true)
            context.fill()
            context.beginPath()
            context.arc(that.centerX, that.centerY, that.ringInnerRadius, 0, 2 * Math.PI, true)
            context.stroke()
            context.restore()
        },
        // 绘制刻度
        drawTicks () {
            const that = this
            const context = that.context
            const hourTickRadius = that.ringInnerRadius - that.hourTickLen
            const minTickRadius = that.ringInnerRadius - that.minTickLen
            const pi = Math.PI
            const startEngle = -(pi / 2)
            const deltEngle = 2 * pi / (4 * 12)
            let startPointX = 0
            let startPointY = 0
            let endPointX = 0
            let endPointY = 0
            let currEngle = startEngle
            let currTickLen
            let num

            context.save()
            context.lineWidth = 1
            context.textAlign = 'center'
            context.textBaseline = 'middle'
            context.font = `normal ${ 12 * that.scale }px 微软雅黑`
            for (let i = 0; i < 48; i++) {
                currTickLen = (i % 4 == 0 ? hourTickRadius : minTickRadius)
                context.lineWidth = (i % 4 == 0 ? 2 : 1)
                context.strokeStyle = 'rgba(0,0,0,0.2)'
                startPointX = that.centerX + Math.cos(currEngle) * currTickLen
                startPointY = that.centerY + Math.sin(currEngle) * currTickLen
                endPointX = that.centerX + Math.cos(currEngle) * that.ringInnerRadius
                endPointY = that.centerY + Math.sin(currEngle) * that.ringInnerRadius
                context.beginPath()
                context.moveTo(startPointX, startPointY)
                context.lineTo(endPointX, endPointY)
                context.stroke()
                if (i % 4 == 0) { // 绘制数字
                    num = i / 4
                    num = (num == 0 ? 12 : num)
                    context.fillStyle = '#6d6d6d'
                    context.fillText(num, that.centerX + Math.cos(currEngle) * (currTickLen - 10), that.centerY + Math.sin(currEngle) * (currTickLen - 10))
                }
                currEngle += deltEngle
            }
            context.restore()
        },
        // 绘制指针
        drawPoints () {
            const that = this
            const context = that.context
            const pi = Math.PI
            const currDate = new Date()
            const hour = currDate.getHours()
            const min = currDate.getMinutes()
            const second = currDate.getSeconds()
            const hourPointLen = 55 * that.scale
            const hourEngle = -(pi / 2) + (2 * pi / 12) * (hour % 12)
            const minPointLen = 85 * that.scale
            const minEngle = -(pi / 2) + (2 * pi / 60) * min
            const secondPointLen = 115 * that.scale
            const secondEngle = -(pi / 2) + (2 * pi / 60) * second
            that.lastTime = currDate.getTime()
            context.save()
            context.lineCap = 'round'
            context.strokeStyle = '#6d6d6d'
            context.lineWidth = 4 * that.scale
            context.beginPath()
            context.moveTo(that.centerX, that.centerY)
            context.lineTo(that.centerX + hourPointLen * Math.cos(hourEngle), that.centerY + hourPointLen * Math.sin(hourEngle))
            context.stroke()

            context.lineWidth = 3 * that.scale
            context.beginPath()
            context.moveTo(that.centerX, that.centerY)
            context.lineTo(that.centerX + minPointLen * Math.cos(minEngle), that.centerY + minPointLen * Math.sin(minEngle))
            context.stroke()

            context.lineWidth = 2 * that.scale
            context.beginPath()
            context.moveTo(that.centerX, that.centerY)
            context.lineTo(that.centerX + secondPointLen * Math.cos(secondEngle), that.centerY + secondPointLen * Math.sin(secondEngle))
            context.stroke()

            let timeStr = (hour < 10 ? '0' + hour : hour) + ' : ' + (min < 10 ? '0' + min : min) + ' : ' + (second < 10 ? '0' + second : second)
            context.font = `normal ${ 14 * that.scale }px 微软雅黑`
            context.textAlign = 'center'
            context.textBaseline = 'middle'
            context.fillStyle = '#6d6d6d'
            context.lineWidth = 1
            context.fillText(timeStr, that.centerX, that.centerY + (45 * that.scale))
            const month = currDate.getMonth() + 1
            const date = currDate.getDate()
            const day = currDate.getDay()
            const weekObj = ['日', '一', '二', '三', '四', '五', '六']
            timeStr = currDate.getFullYear() + ' / ' + (month < 10 ? '0' + month : month) + ' / ' + (date < 10 ? '0' + date : date) + ' 星期' + weekObj[day]
            context.fillText(timeStr, that.centerX, that.centerY + (75 * that.scale))
            context.restore()
        }
    }
}
</script>

<style lang="scss">
    @use "./style.scss";
</style>
